/**
 * 
 */
package com.gitblit.utils;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jgit.transport.ReceiveCommand.Result;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.slf4j.Logger;

import com.gitblit.GitBlit;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;

/**
 * @author Bert Chen
 *
 *	This checker reads custom fields in repository config to determine each branch's permission.
 *	It follows "allow by default" policy as original design in "blockpush.groovy" script, 
 *	any branches configured into permission config will be then treated as "deny by default unless user/team matches the permission settings".
 *
 *	Repository owners (including personal repo) and admins are ignored in check logic.
 *	
 *	To simply the design, this checker encapsulates control logics, no Gitblit models will be modified.
 *	Current implementation will abort all subsequent Groovy scripts if any one of ReceiveCommand is failed.
 *
 *	How to Use:
 *		1. Hook "branch-permissions.groovy" to repositories which require branch level control
 *		2. Edit config file in remote bare repository and add permission settings. (see bellow)
 *
 *	Configuration formats in repository config:
 *
 *		[gitblit "customFields"]
 *		# Deny push by default in all branches; members in team "architects" are allowed to push to all branches
 *		# If you leave the value as blank, it will reject all push requests, only admins, owners, and users/teams configured 
 *		# in detail level can push.
 *		 branch-permission-all = architects@team
 *		
 *		# Deny push branch "refs/heads/push_restriction" by default; user "junior" and members in team "qa" is allowed to push
 *		 branch-name-1 = refs/heads/push_restriction
 *		 branch-permission-1 = junior@user,qa@team
 *
 *		# Allow users - senior and junior - to push to branch xxxxxx
 *		 branch-name-2 = refs/heads/xxxxxx
 *		 branch-permission-2 = senior@user,junior@user
 *
 * To-Do: 
 * 		- To optimize permission checks in one single loop
 * 		- Gitblit Web UI for setting up branch permissions
 */
public class BranchPermissionChecker {
	
	private static final String BRANCH_PERMISSION_PREFIX = "branch-permission-";
	private static final String BRANCH_NAME_PREFIX = "branch-name-";	
	private static final String PERMITTED_ROLES_DELIMITER = ",";
	private static final String PERMITTED_USER_POSTFIX = "@user";
	private static final String PERMITTED_TEAM_POSTFIX_ = "@team";
	
	/* Notes:
	 * 	Use GitBlit.self() to acquire Gitblit services
	 */
	public static boolean hasPermission(GitBlit gitblit, RepositoryModel repository, ReceivePack receivePack, UserModel user, 
			Collection<ReceiveCommand> commands, Logger logger) {
		
		// continue groovy scripts by default
		boolean isContinue = true;
		
		// Check user privileges first
		// Owners and Admins will skip further checks
		if (user.canAdmin || repository.isOwner(user.username) || repository.isUsersPersonalRepository(user.username))
			return isContinue;
		
		// Debugging info
		RegistrantAccessPermission repoPermission = user.getRepositoryPermission(repository);
		logger.debug(user.username + " has " + repoPermission.permission + " in " + repository.name);
		
		String updatedRef = null;		
		Map<String, String> wildcardPermissions = null;
		Map<String, String> branchMap = getBranchMap(repository.customFields);
		
		if (repository.customFields.containsKey(BRANCH_PERMISSION_PREFIX + "all")) {
			// if wildcard config is set, whole branches become "deny as default"
			// users and teams in wildcard setting have permissions to all branches
			wildcardPermissions = getPermissionMap(repository.customFields.get(BRANCH_PERMISSION_PREFIX + "all"));
		}
		for (ReceiveCommand command : commands) {
			boolean isPermitted = true;
			Map<String, String> branchPermissions = null;
			updatedRef = command.getRefName();
			if (branchMap.containsKey(updatedRef)) {
				logger.debug("Try to get permissions for " + updatedRef);
				branchPermissions = getPermissionMap(branchMap.get(updatedRef));
			}
			if (branchPermissions != null) {
				// Check branch level permission
				// Check username
				logger.debug("Looking for " + user.username + PERMITTED_USER_POSTFIX + " in branch permissions map.");
				if (!branchPermissions.containsKey(user.username + PERMITTED_USER_POSTFIX)) {
					// Check teams in user model
					boolean teamPermissionFound = false;
					for (TeamModel team : user.teams) {
						logger.debug("Looking for " + team.name + PERMITTED_TEAM_POSTFIX_ + " in branch permissions map.");
						if (branchPermissions.containsKey(team.name + PERMITTED_TEAM_POSTFIX_)) {
							teamPermissionFound = true;
							break;
						}
					}
					if (!teamPermissionFound)
						isPermitted = false;
				}
			}
			if ((branchPermissions == null || !isPermitted) && wildcardPermissions != null) {
				// Check wildcard permission
				logger.debug("Looking for " + user.username + PERMITTED_USER_POSTFIX + " in wildcard permissions map.");
				if (!wildcardPermissions.containsKey(user.username + PERMITTED_USER_POSTFIX)) {
					boolean teamPermissionFound = false;
					for (TeamModel team : user.teams) {
						logger.debug("Looking for " + team.name + PERMITTED_TEAM_POSTFIX_ + " in wildcard permissions map.");
						if (wildcardPermissions.containsKey(team.name + PERMITTED_TEAM_POSTFIX_)) {
							teamPermissionFound = true;
							break;
						}
					}
					if (!teamPermissionFound)
						isPermitted = false;
					else
						// rollback
						isPermitted = true;
					
				}
				else
					// rollback
					isPermitted = true;
			}
			
			// Final check
			if (!isPermitted) {
				command.setResult(Result.REJECTED_OTHER_REASON, "You are not permitted to write to reference " + updatedRef + " in " + repository.name);
				// set isContinue to false if any one of command is failed
				if (isContinue)
					isContinue = false;
			}
		}
		return isContinue;
	}
	
	private static Map<String, String> getBranchMap(Map<String, String> customFields) {
		Map<String, String> map = new HashMap<String, String>();
		for (String key : customFields.keySet()) {
			key = key.toLowerCase();
			if (key.startsWith(BRANCH_NAME_PREFIX)) {
				map.put(customFields.get(key), 
						customFields.get(BRANCH_PERMISSION_PREFIX + key.substring(BRANCH_NAME_PREFIX.length())));
			}
		}
		return map;
	}
	
	private static Map<String, String> getPermissionMap(String permissionString) {
		Map<String, String> map = new HashMap<String, String>();
		if (permissionString != null && !permissionString.trim().equals("")) {
			String[] tokens = permissionString.trim().split(PERMITTED_ROLES_DELIMITER);
			for (String token : tokens) {
				token = token.trim().toLowerCase();
				// For the time being, put NULL in value.
				map.put(token, null);
			}
		}
		return map;
	}
	
}
